function vec_sub( a, b ) {
    return [    a[0] - b[0],
                a[1] - b[1],
                a[2] - b[2]
                ];
}

function cross_product( a, b ) {
    return [    a[1]*b[2] - a[2]*b[1],
                a[2]*b[0] - a[0]*b[2],
                a[0]*b[1] - a[1]*b[0]
                ];
}

function vec_len( vec ) {
    return Math.sqrt( vec[0]*vec[0] + vec[1]*vec[1] + vec[2]*vec[2] );
}

function vec_norm( vec ) {
    var l = vec_len( vec );
    return (l) ? [ vec[0]/l, vec[1]/l, vec[2]/l ] : vec;
}

function calc_angle( a, b ) {
    var ab = a[0]*b[0] + a[1]*b[1] + a[2]*b[2];
    var c = ab / vec_len(a) / vec_len(b);
    return Math.acos( c );
}

function Matrix3D() {
/*
 *  Матрица 4x4 для векторных операций.
 *  В реализации применяется ускоренная безматричная математика
 *    с развернутыми циклами
 */
    this.matrix = [];

    this.loadIdentity = function() {
        this.matrix = [ 1, 0, 0, 0,
                        0, 1, 0, 0,
                        0, 0, 1, 0,
                        0, 0, 0, 1 ];
        return this;
    }

    // умножение матрицы на вектор из 3х чисел
    // на выходе - вектор входной размерности
    this.mul3v = function( vec3 ) {
        var x = vec3[0] || 0;
        var y = vec3[1] || 0;
        var z = vec3[2] || 0;

        var X = this.matrix[0]*x + this.matrix[1]*y + this.matrix[ 2]*z + this.matrix[ 3];
        var Y = this.matrix[4]*x + this.matrix[5]*y + this.matrix[ 6]*z + this.matrix[ 7];
        var Z = this.matrix[8]*x + this.matrix[9]*y + this.matrix[10]*z + this.matrix[11];

        return [ X, Y, Z ];
    }

    // умножение матрицы на матрицу
    this.mul4x4v = function( mat ) {
        var res = new Array(16);
        for ( var r=0; r<4; r++ ) {
            var r4 = r*4;
            for ( var c=0; c<4; c++ ) {
                // развернул третий цикл
                res[ r4+c ] =   this.matrix[ r4+0 ] * mat[ 0*4+c ] +
                                this.matrix[ r4+1 ] * mat[ 1*4+c ] +
                                this.matrix[ r4+2 ] * mat[ 2*4+c ] +
                                this.matrix[ r4+3 ] * mat[ 3*4+c ];
            }
        }
        this.matrix = res;
        return this;
    }

    this.translate = function( dx, dy, dz ) {
        var mat = [  1,  0,  0, dx,
                     0,  1,  0, dy,
                     0,  0,  1, dz,
                     0,  0,  0,  1 ];

        this.mul4x4v( mat );
        return this;
    }

    this.scale = function( dx, dy, dz ) {
        dy = dy||dx;
        dz = dz||dx;
        var mat = [ dx,  0,  0,  0,
                     0, dy,  0,  0,
                     0,  0, dz,  0,
                     0,  0,  0,  1 ];

        this.mul4x4v( mat );
        return this;
    }

    this.rotateX = function( angle ) {
        var s = Math.sin( angle );
        var c = Math.cos( angle );

        var mat = [  1,  0,  0,  0,
                     0,  c, -s,  0,
                     0,  s,  c,  0,
                     0,  0,  0,  1 ];

        this.mul4x4v( mat );
        return this;
    }

    this.rotateY = function( angle ) {
        var s = Math.sin( angle );
        var c = Math.cos( angle );

        var mat = [  c,  0, -s,  0,
                     0,  1,  0,  0,
                     s,  0,  c,  0,
                     0,  0,  0,  1 ];

        this.mul4x4v( mat );
        return this;
    }

    this.rotateZ = function( angle ) {
        var s = Math.sin( angle );
        var c = Math.cos( angle );

        var mat = [  c, -s,  0,  0,
                     s,  c,  0,  0,
                     0,  0,  1,  0,
                     0,  0,  0,  1 ];

        this.mul4x4v( mat );
        return this;
    }

    this.rotate = function( aX, aY, aZ ) {
        // пока так вот тупо, все равно не использую
        return this.rotateX( aX ).rotateY( aY ).rotateZ( aZ );
    }

    this.loadIdentity();
}

function VBO( vtx, idx ) {
    this.vertexes = vtx || [];
    this.indexes = idx || [];

    this.append = function( vtx, idx ) {
        this.vertexes = this.vertexes.concat( vtx );
        this.indexes = this.indexes.concat( vtx );
        return this;
    }

    this.clear = function() {
        this.vertexes = [];
        this.indexes = [];
        return this;
    }
}

function Camera() {
    this.location = [0, 0, -1];
    this.target  = [0, 0,  0];

    var PI  = Math.PI;
    var PI2 = Math.PI/2;

    this.get_angle = function( vertex1, vertex2, vertex3 ) {
        var v1 = vec_norm( vec_sub( vertex1, vertex2 ) );
        var v2 = vec_norm( vec_sub( vertex1, vertex3 ) );
        var normal = vec_norm( cross_product( v1, v2 ) );

        var v = vec_norm( vec_sub( vertex1, this.location ) );
        return calc_angle( normal, v );
    }

    this.test_visibility_by_normal = function( vertex1, vertex2, vertex3 ) {
        var a = this.get_angle( vertex1, vertex2, vertex3 );
        return ( a > PI2 ) && ( a < PI+PI2 );
    }

    this.do_projection = function( vec ) {
        if ( this.location[2] == vec[2] ) return vec; // хотя можно возвращать false, [ NaN, NaN, NaN ], и вообще что угодно

        var k = ( this.location[2] - this.target[2] ) / ( this.location[2] - vec[2] );

        var x = vec[0] * k;
        var y = vec[1] * k;
        var z = vec[2] - this.target[2];

        return vec_norm( [ x, y, z ] );
    }
}

// Canvas3D.oDrawMode
var C3D_DRAW_MODE_POINTS = 1;
var C3D_DRAW_MODE_LINES  = 2;
var C3D_DRAW_MODE_FILL   = 3;

function Canvas3D( canvasElement ) {
    this.canvas = canvasElement;
    this.FPS = 0;
    this.drawNow = false;

    // мировая матрица для перевода в локальные координаты
    this.ModelView = new Matrix3D();
    // камера
    this.camera = new Camera();

    this.oCullFace = false;
    this.oCullFaceBack = true;

    this.oDrawMode = C3D_DRAW_MODE_LINES;

    this.VertexCacheSize = 20;


    this.drawVBO = function( VBO ) {
        return this.drawVBOsep( VBO.vertexes, VBO.indexes );
    }

    this.drawVBOsep = function( vertexes, indexes ) {
        var vertex_cache = {};
        var vertex_cache_uses = {};
        var idx_len = indexes.length;
        var vtx_len = vertexes.length;
        if ( idx_len%3 || vtx_len%3 ) return false;

        var self = this;

        // выборка вершины из кеша спроектированных данных
        function fetch_vertex_MV( index ) {
            var vec = vertexes.slice( 3*index, 3*index+3 );
            // перевод вершины в локальную СК
            vec = self.ModelView.mul3v( vec );
/*
            if ( !( index in vertex_cache ) ) {
                var vec = vertexes.slice( 3*index, 3*index+3 );
                // перевод вершины в локальную СК
                vec = self.ModelView.mul3v( vec );

                // если достигнут предел кеша - ищу чтобы такого удалить из него
                if ( vertex_cache.length == self.VertexCacheSize ) {
                    // индекс удаляемого элемента
                    var to_delete = 0;
                    // число использований удаляемого элемента
                    var to_delete_uses = 9999999;
                    // ищу индекс с наименьшим числом использований
                    for ( var i in vertex_cache_uses ) {
                        var uses = vertex_cache_uses[i];
                        if ( uses < to_delete_uses ) {
                            to_delete = i;
                            to_delete_uses = uses;
                        }
                    }

                    // произвожу "удаление"
                    vertex_cache_uses[to_delete] = undefined;
                    vertex_cache[to_delete] = undefined;
                }

                vertex_cache[index] = vec;
                vertex_cache_uses[index] = 1;
            }
            else {
                vertex_cache_uses[index]++;
            }

            var vec = vertex_cache[index];
*/
            return [ vec[0], vec[1], vec[2] ];
        }

        for ( var i=0; i<idx_len; i+=3 ) {
            // три индекса
            var idx = indexes.slice( i, i+3 );
            // три вершины по этим индексам
            var vec1 = fetch_vertex_MV( idx[0] );
            var vec2 = fetch_vertex_MV( idx[1] );
            var vec3 = fetch_vertex_MV( idx[2] );

            // тест видимости полигона
            //if ( this.oCullFace ) {
                var visible = this.camera.test_visibility_by_normal( vec1, vec2, vec3 );
                // ( v & !cb ) | ( !v & cb ) === v ^ cb
                //if ( visible ^ this.oCullFaceBack ) continue;
                context.fillStyle = ( visible ^ this.oCullFaceBack ) ? 'red' : 'green';
            //}

            // перевод из локальной СК в экранную (проецирование)
            vec1 = this.camera.do_projection( vec1 );
            vec2 = this.camera.do_projection( vec2 );
            vec3 = this.camera.do_projection( vec3 );

            // перевод из экранной СК в СК канвы
            vec1[0] = parseInt( ( vec1[0]+1 ) * _width/2 );
            vec1[1] = parseInt( ( vec1[1]+1 ) * _height/2 );

            vec2[0] = parseInt( ( vec2[0]+1 ) * _width/2 );
            vec2[1] = parseInt( ( vec2[1]+1 ) * _height/2 );

            vec3[0] = parseInt( ( vec3[0]+1 ) * _width/2 );
            vec3[1] = parseInt( ( vec3[1]+1 ) * _height/2 );

            // вывожу треугольник
            context.beginPath();
            context.moveTo( vec1[0], vec1[1] );
            context.lineTo( vec2[0], vec2[1] );
            context.lineTo( vec3[0], vec3[1] );
            context.lineTo( vec1[0], vec1[1] );
            context.fill();
        }

/*
        // перевод вершин в локальную СК
        var vtx_buf = new Array( vtx_len );
        var vtx_tmp = new Array( vtx_len );
        for ( var i=0; i<vtx_len; i+=3 ) {
            var vec = vertexes.slice( i, i+3 );

            vec = this.ModelView.mul3v( vec );

            vtx_tmp[i+0] = vtx_buf[i+0] = vec[0];
            vtx_tmp[i+1] = vtx_buf[i+1] = vec[1];
            vtx_tmp[i+2] = vtx_buf[i+2] = vec[2];
        }

        // перевод из локальной СК в экранную (проецирование)
        for ( var i=0; i<vtx_len; i+=3 ) {
            var vec = vtx_buf.slice( i, i+3 );

            vec = this.camera.do_projection( vec );

            vtx_buf[i+0] = vec[0];
            vtx_buf[i+1] = vec[1];
            vtx_buf[i+2] = vec[2];
        }

        // перевод из экранной СК в СК канвы
        for ( var i=0; i<vtx_len; i+=3 ) {
            var vec = vtx_buf.slice( i, i+3 );

            vtx_buf[i+0] = parseInt( ( vec[0]+1 ) * _width/2 );
            vtx_buf[i+1] = parseInt( ( vec[1]+1 ) * _height/2 );
        }

        // немного смягчает линии
        var gA = context.globalAlpha;
        context.globalAlpha = 0.5;

        // вывожу объект
        for ( var i=0; i<idx_len; i+=3 ) {
            var idx = indexes.slice( i, i+3 );

            // тест видимости полигона
            if ( this.oCullFace ) {
                var vec1 = vtx_tmp.slice( 3*idx[0], 3*idx[0]+3 );
                var vec2 = vtx_tmp.slice( 3*idx[1], 3*idx[1]+3 );
                var vec3 = vtx_tmp.slice( 3*idx[2], 3*idx[2]+3 );
                var visible = this.camera.test_visibility_by_normal( vec1, vec2, vec3 );
                var angle = this.camera.get_angle( vec1, vec2, vec3 );
                // ( v & !cb ) | ( !v & cb ) === v ^ cb
                //if ( visible ^ this.oCullFaceBack ) continue;
            }
            else {
                var vec1 = vtx_tmp.slice( 3*idx[0], 3*idx[0]+3 );
                var vec2 = vtx_tmp.slice( 3*idx[1], 3*idx[1]+3 );
                var vec3 = vtx_tmp.slice( 3*idx[2], 3*idx[2]+3 );
                var angle = this.camera.get_angle( vec1, vec2, vec3 );
            }

            var vec1 = vtx_buf.slice( 3*idx[0], 3*idx[0]+3 );
            var vec2 = vtx_buf.slice( 3*idx[1], 3*idx[1]+3 );
            var vec3 = vtx_buf.slice( 3*idx[2], 3*idx[2]+3 );

            var color = parseInt( ( angle - Math.PI/2 ) * 255 / Math.PI );
            color = 'rgb('+color+','+color+','+color+')';

            var fS = context.fillStyle;
            context.fillStyle = color;

            if ( this.oDrawMode == C3D_DRAW_MODE_LINES || this.oDrawMode == C3D_DRAW_MODE_FILL ) {
                context.beginPath();
                context.moveTo( vec1[0], vec1[1] );
                context.lineTo( vec2[0], vec2[1] );
                context.lineTo( vec3[0], vec3[1] );
                context.lineTo( vec1[0], vec1[1] );

                if ( this.oDrawMode == C3D_DRAW_MODE_LINES ) {
                    context.stroke();
                }
                else {
                    context.fill();
                }
            }
            else {
                // вывод точек вертексов
            }

            context.fillStyle = fS;
        }

        context.globalAlpha = gA;
*/
        return true;
    }

    var time = function() {
        return (new Date()).getTime();
    }

    this.pushMatrix = function() {
        context.save();
    };

    this.popMatrix = function() {
        context.restore();
    };

    this.width = function() {
        return _width;
    }

    this.height = function() {
        return _height;
    }

    var calc_fps = function( canvas ) {
        var diff = time() - timeSinceLastFPS;
        ++framesSinceLastFPS;
        ++frameCount;

        if ( diff > 500 ) {
            canvas.FPS = 1000.0 * framesSinceLastFPS / diff;
            timeSinceLastFPS = time();
            framesSinceLastFPS = 0;
        }
    }

    this.internal_draw = function() {
        // чтобы не допустить повторного не обоснованного вызова
        timeSinceLastLazyDraw = time();

        while ( 1 ) {
            // подсчет FPS
            calc_fps(this);

            this.drawNow = true;
                context.save();
                    this.draw( context, _width, _height );
                context.restore();
            this.drawNow = false;

            // сколько прошло с момента прошлого вызова
            var diff = time() - timeSinceLastLazyDraw;
            // задержка равна желаемой минус уже истраченное время на текущей итерации
            var interval = 1000/expectFPS - diff;
            timeSinceLastLazyDraw = time();

            // если задержка есть - ожидаю
            if ( interval > 0 ) {
                var self = this;
                timeoutId = setTimeout( function() { self.internal_draw(); }, interval );
                break;
            }
            else {
                // если задержки нет, то это либо первый кадр,
                // либо не укладываемся в отведенный FPS и уже не до ожиданий
                continue;
            }
        }
    }

    this.draw = function( ctx, width, height ) {
        // заглушка на рисование в виде очистки
        // всей области вывода цветом RGBA( ?, ?, ?, 0 )
        ctx.clearRect( 0, 0, width, height );
    }

    this.main_loop = function( expectFPSValue ) {
        // если таймер уже есть - это повторный вызов
        if ( timeoutId ) return false;
        // если ожидаемый FPS не натуральный, то рассматриваем это,
        // как требование выжать максимум, и выставляем 100.000 FPS :)
        expectFPS = ( expectFPSValue > 0 ) ? parseInt(expectFPSValue) : 100000;

        // вызов отрисовки с заданием времени прежней в 1970 году :)
        timeSinceLastLazyDraw = 0;
        this.internal_draw();
    }

    // метод для баловства: рисую линию
    this.line2d = function( x1, y1, x2, y2 ) {
        context.beginPath();
            context.moveTo( x1+0.5, y1+0.5 );
            context.lineTo( x2+0.5, y2+0.5 );
        context.stroke();
    }

    // метод для баловства: рисую точку
    this.point4i = function( x, y, R, G, B, A ) {
        var ID = context.createImageData( 1, 1 );
        ID.data[0] = R;
        ID.data[1] = G;
        ID.data[2] = B;
        ID.data[3] = A || 255;
        context.putImageData( ID, x, y );
    }

    // временный метод
    this.getContext = function() {
        return context;
    }

    var context = canvasElement.getContext( '2d' );
    var
        _width = parseInt( this.canvas.width ),
        _height = parseInt( this.canvas.height ),
        timeoutId = 0,
        expectFPS = 10,
        timeSinceLastLazyDraw = 0,
//          started_at = (new Date()).getTime(),
        timeSinceLastFPS = (new Date()).getTime(),
        framesSinceLastFPS = 0,
        frameCount = 0;
}
